// @ts-expect-error: Could not find type declarations for babel-types
import * as _t from "../../lib/index.js";
import {
  isNullable,
  hasDefault,
  sortFieldNames,
} from "../utils/fieldHelpers.ts";
import stringifyValidator, {
  isValueType,
} from "../utils/stringifyValidator.ts";
import type { FieldOptions, Validator } from "../../src/definitions/utils.ts";

const t = _t as typeof import("@babel/types");

const parentMaps = new Map([["File", new Set(["null"])]]);

/**
 * Register parent maps for a given parent and an array of nodes.
 * @param parent
 * @param nodes
 */
function registerParentMaps(parent: string, nodes: string[]) {
  for (const node of nodes) {
    if (!parentMaps.has(node)) {
      parentMaps.set(node, new Set());
    }
    parentMaps.get(node).add(parent);
  }
}

/**
 * Get the node types from a validator. It will also resolve alias types to their
 * corresponding node types.
 * @param validator
 * @returns
 */
function getNodeTypesFromValidator(validator: Validator | undefined): string[] {
  if (validator === undefined) return [];
  if ("each" in validator) {
    return getNodeTypesFromValidator(validator.each);
  }
  if ("chainOf" in validator) {
    return getNodeTypesFromValidator(validator.chainOf[1]);
  }

  let nodeTypes: string[] = [];
  if ("oneOfNodeTypes" in validator) {
    nodeTypes = validator.oneOfNodeTypes.slice();
  }
  if ("oneOfNodeOrValueTypes" in validator) {
    nodeTypes = validator.oneOfNodeOrValueTypes.filter(
      type => !isValueType(type)
    );
  }
  return nodeTypes.flatMap(type => t.FLIPPED_ALIAS_KEYS[type] ?? type);
}

export default function generateAstTypes() {
  let code = `// NOTE: This file is autogenerated. Do not modify.
// See packages/babel-types/scripts/generators/ast-types.ts for script used.

interface BaseComment {
  value: string;
  start?: number;
  end?: number;
  loc?: SourceLocation;
  // generator will skip the comment if ignore is true
  ignore?: boolean;
  type: "CommentBlock" | "CommentLine";
}

interface Position {
  line: number;
  column: number;
  index: number;
}

export interface CommentBlock extends BaseComment {
  type: "CommentBlock";
}

export interface CommentLine extends BaseComment {
  type: "CommentLine";
}

export type Comment = CommentBlock | CommentLine;

export interface SourceLocation {
  start: Position;
  end: Position;
  filename: string;
  identifierName: string | undefined | null;
}

interface BaseNode {
  leadingComments?: Comment[] | null;
  innerComments?: Comment[] | null;
  trailingComments?: Comment[] | null;
  start?: number | null;
  end?: number | null;
  loc?: SourceLocation | null;
  range?: [number, number];
  extra?: Record<string, unknown>;
}

export type CommentTypeShorthand = "leading" | "inner" | "trailing";

export type Node = ${t.TYPES.filter((k: string) => !t.FLIPPED_ALIAS_KEYS[k])
    .sort()
    .join(" | ")};\n\n`;

  const deprecatedAlias: Record<string, string> = {};
  for (const type in t.DEPRECATED_KEYS) {
    deprecatedAlias[t.DEPRECATED_KEYS[type]] = type;
  }
  for (const type in t.NODE_FIELDS) {
    const fields = t.NODE_FIELDS[type];
    const unionShape = t.NODE_UNION_SHAPES__PRIVATE[type];
    const fieldNames = sortFieldNames(Object.keys(t.NODE_FIELDS[type]), type);

    const struct: string[] = [];

    fieldNames.forEach(fieldName => {
      const field: FieldOptions = fields[fieldName];
      registerParentMaps(type, getNodeTypesFromValidator(field.validate));

      if (
        unionShape?.discriminator === fieldName ||
        unionShape?.shapes[0].properties[fieldName]
      ) {
        return;
      }

      let typeAnnotation = stringifyValidator(field.validate, "");

      if (isNullable(field) && !hasDefault(field)) {
        typeAnnotation += " | null";
      }

      const alphaNumeric = /^\w+$/;
      const optional = field.optional ? "?" : "";

      if (field.deprecated) {
        struct.push("/** @deprecated */");
      }
      if (t.isValidIdentifier(fieldName) || alphaNumeric.test(fieldName)) {
        struct.push(`${fieldName}${optional}: ${typeAnnotation};`);
      } else {
        struct.push(`"${fieldName}"${optional}: ${typeAnnotation};`);
      }
    });

    if (unionShape) {
      const types: string[] = unionShape.shapes.map(shape => {
        const name = `${type + shape.name.charAt(0).toUpperCase() + shape.name.slice(1)}`;
        code += `export interface ${name} extends BaseNode {
  type: "${type}";
  ${struct.join("\n")}
  ${unionShape.discriminator}: ${shape.value.map(v => JSON.stringify(v)).join("|")};
  ${Object.entries(shape.properties)
    .map(([key, value]) => `${key}: ${stringifyValidator(value.validate, "")};`)
    .join("\n  ")}
}\n`;
        return name;
      });

      code += `export type ${type} = ${types.join(" | ")}\n\n`;
    } else {
      code += `export interface ${type} extends BaseNode {
  type: "${type}";
  ${struct.join("\n  ")}
}\n\n`;
    }

    if (deprecatedAlias[type]) {
      code += `/**
 * @deprecated Use \`${type}\`
 */
export interface ${deprecatedAlias[type]} extends BaseNode {
  type: "${deprecatedAlias[type]}";
  ${struct.join("\n  ").trim()}
}\n\n
`;
    }
  }

  for (const type in t.FLIPPED_ALIAS_KEYS) {
    const types: string[] = t.FLIPPED_ALIAS_KEYS[type];
    code += `export type ${type} = ${types
      .map(type => `${type}`)
      .join(" | ")};\n`;
  }
  code += "\n";

  code += "export interface Aliases {\n";
  for (const type in t.FLIPPED_ALIAS_KEYS) {
    code += `  ${type}: ${type};\n`;
  }
  code += "}\n\n";
  code += `export type DeprecatedAliases = ${Object.keys(
    t.DEPRECATED_KEYS
  ).join(" | ")}\n\n`;

  code += "export interface ParentMaps {\n";

  registerParentMaps("null", [...Object.keys(t.DEPRECATED_KEYS)]);
  // todo: provide a better parent type for Placeholder, currently it acts
  // as a catch-all parent type for an abstract NodePath, s.t NodePath.parent must
  // be a Node if type has not been specified
  registerParentMaps("Node", ["Placeholder"]);

  const parentMapsKeys = [...parentMaps.keys()].sort();
  for (const type of parentMapsKeys) {
    const deduplicated = [...parentMaps.get(type)].sort();
    code += `  ${type}: ${deduplicated.join(" | ")};\n`;
  }
  code += "}\n\n";

  return code;
}
